﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Globalization;
using System.IO;
using SpectationClient.Stuff;

namespace SpectationClient.SpectralTools {
    
    public class ENVI_FileReader {

        protected static CultureInfo numericCulture = CultureInfo.CreateSpecificCulture("en-US");
        #region enumerations for special values
        /// <summary>
        /// BSQ = Band sequential: each line followed by next line in same spectral band
        /// BIP = first pixels of all band in sequential order, than second pixels ...
        /// BIL = first line of all bands, sencond line of all bands, ...
        /// </summary>
        public enum Interleave : byte {
            BSQ = 0, //Band sequential 
            BIP = 1,
            BIL = 2,
            unknown = 255
        }

       

        public enum IDLDataType : byte {
            Byte_byte = 1,
            Int16_int= 2,
            Int32_long= 3,
            Single_float= 4,
            Double_double= 5,
            Complex32Bit_complex= 6,
            Complex64Bit_dcomplex= 7,
            UInt16_uint= 12,
            UInt32_ulong= 13,
            Int64_long64= 14,
            UInt64_ulong64= 15
        }

        public enum ByteOrder : byte {
            /// <summary>
            /// least significant byte first (LSF)
            /// </summary>
            Host_Intel_LSF = 0,
            /// <summary>
            /// Most significant byte first (MSF)
            /// </summary>
            Network_IEEE_MSF = 1,
            unknown = 255
        }
        #endregion


        #region constant definition of meta tag names and corresponding variables (all lower case)
        protected const string c_band_names = "band names";
        protected const string c_bands = "bands";
        protected const string c_samples = "samples";
        protected const string c_bbl = "bbl";
        protected const string c_byte_order = "byte order";
        protected const string c_data_ignore_value = "data ignore value";
        protected const string c_data_type = "data type";
        protected const string c_description = "description";
        protected const string c_file_type = "file type";
        protected const string c_interleave = "interleave";
        protected const string c_lines = "lines";
        protected const string c_fwhm = "fwhm";
        protected const string c_header_offset = "header offset";
        protected const string c_data_gain_values = "data gain values";
        protected const string c_data_offset_values = "data offset values";

        protected static string[] requiredTags = {  c_bands, c_lines, c_samples, c_interleave, c_data_type, 
                                                    c_file_type, c_byte_order, c_header_offset };

        //public static const string c_class_lookup = "class lookup";
        //public static const string c_class_names = "class names";
        //public static const string c_classes = "classes";
        //public static const string c_default_bands = "default bands";
        //public static const string c_default_stretch = "default stretch";
        //public static const string c_dem_band = "dem band";
        //public static const string c_dem_file = "dem file";    
        //public static const string c_geo_points = "geo points";
        //public static const string c_map_info = "map info";
        //public static const string c_pixel_size = "pixel_size";
        //public static const string c_major_frame_offsets = "major frame_offsets";
        //public static const string c_minor_frame_offsets = "minor frame_offsets";
        //public static const string c_projection_info = "projection info";
        //public static const string c_reflectance_scale_factor = "reflectance scale factor";
        //public static const string c_rpc_info = "rpc info";
        //public static const string c_sensor_type = "sensor type";
        //public static const string c_spectra_names = "spectra names";
        //public static const string c_wavelength = "wavelength";
        //public static const string c_wavelength_units = "wavelength units";
        //public static const string c_x_start = "x start";
        //public static const string c_y_start = "y start";
        //public static const string c_z_plot_average = "z plot average";
        //public static const string c_z_plot_range = "z plot range";
        //public static const string c_z_plot_titles = "z plot titles";
        
        #endregion

            //private Dictionary<String, Object> HeaderTags = new Dictionary<string, object>();
            protected ENVI_HeaderParser ESLParser = new ENVI_HeaderParser();
            //private Byte[] mainBlob;
           
            protected SPECTATION_BinaryReader BR;

            protected Int64 FileSize { get { return BR.Length; } }
            protected struct RequiredHeaderValues {
                public Int16 bands;
                public Int32 lines;
                public Int32 samples;
                public Interleave interleave;
                public IDLDataType idlDataType;
                public Int16 dataTypeByteSize;
                public String fileType;
                public ByteOrder byteOrder;
                public int headerOffset;
                public String description;

               
               
            }
            protected RequiredHeaderValues HV = new RequiredHeaderValues();
            
            public ENVI_FileReader(FileInfo blob, FileInfo header) {
                if(!header.Exists) throw new Exception(String.Format("File {0} does not exist", header.Name));
                this.setHeader(File.ReadAllText(header.FullName), blob.Length);
                BR = new SPECTATION_BinaryReader(blob, HV.idlDataType, HV.headerOffset);
            }

            public ENVI_FileReader(Byte[] blob, Byte[] headerBlob) {

                String header = ASCIIEncoding.ASCII.GetString(headerBlob);
                this.setHeader(header, blob.LongLength);
                BR = new SPECTATION_BinaryReader(blob, HV.idlDataType, HV.headerOffset);
            }

            public ENVI_FileReader(Byte[] blob, String header) {
                this.setHeader(header, blob.LongLength);
                BR = new SPECTATION_BinaryReader(blob, HV.idlDataType, HV.headerOffset);
            }


            public static bool isValidFile(String path) {
                return ENVI_FileReader.isValidFile(new FileInfo(path));
            }

            public static bool isValidFile(FileInfo fi) {
                FileInfo temp;
                return ENVI_FileReader.isValidFile(fi, out temp);
            }

            public static bool isValidFile(FileInfo fi, out FileInfo headerFile) {
                FileInfo binaryFile;
                List<String> errors;
                RequiredHeaderValues HV;
                if(!ENVI_FileReader.findFileInfos(fi, out binaryFile, out headerFile)) return false;
                String hdrText = File.ReadAllText(headerFile.FullName);
                return ENVI_FileReader.checkHeader(hdrText, fi.Length, out errors, out HV);
            }

            protected static bool checkHeader(String header, Int64 lengthOfBinaryFile, out List<String> errors, out ENVI_FileReader.RequiredHeaderValues HV) {
                ENVI_HeaderParser eslParser = new SpectralTools.ENVI_HeaderParser();
                HV = new RequiredHeaderValues();
                Dictionary<String, String> tags = eslParser.parse(header, out errors);
                if(errors.Count > 0) return false;
                return ENVI_FileReader.checkHeader(tags, lengthOfBinaryFile, out errors, out HV);
            }
            protected static bool checkHeader(Dictionary<String, String> tags, Int64 lengthOfBinaryFile, out List<String> errors, out ENVI_FileReader.RequiredHeaderValues HV) {
                HV = new RequiredHeaderValues();
                errors = new List<string>();

                //Check for required tag
                List<String> missing = (from tag in ENVI_FileReader.requiredTags 
                                        where !tags.Keys.Contains(tag)
                                        select tag).ToList();
                if(missing.Count > 0) {
                    errors.Add(String.Format("Undefined meta tags:{0}", TextHelper.combine(missing, ",")));
                    return false;
                }
                //Check if required tags can get parsed
                foreach(String tagName in tags.Keys) {
                    String tagValueString = tags[tagName];
                    try {
                        switch(tagName) {
                            case c_bands: HV.bands = Int16.Parse(tagValueString); break;
                            case c_lines: HV.lines = Int16.Parse(tagValueString); break;
                            case c_samples: HV.samples = Int16.Parse(tagValueString); break;
                            case c_data_type: HV.idlDataType = ENVI_FileReader.ParseIDLDataType(tagValueString); break;
                            case c_file_type: HV.fileType = tagValueString.Trim(); break;
                            case c_interleave: HV.interleave = ENVI_FileReader.ParseInterleave(tagValueString); break;
                            case c_header_offset: HV.headerOffset = Int32.Parse(tagValueString); break;
                            case c_description: HV.description =  tagValueString; break;
                            case c_byte_order: HV.byteOrder = ENVI_FileReader.ParseByteOrder(tagValueString); break;
                        }
                    } catch {
                        errors.Add(String.Format("Unable to read meta tag {0}", tagName));
                    }
                }
                if(errors.Count > 0) return false;
                //set the dataTypeByteSize
                HV.dataTypeByteSize = ENVI_FileReader.getDataTypeByteSize(HV.idlDataType);

                //check file size
                Int32 expectedSize = HV.headerOffset +  (HV.samples * HV.lines * HV.bands * HV.dataTypeByteSize);
                if(expectedSize != lengthOfBinaryFile) {
                    errors.Add(
                        String.Format("Expected size of binary file/blob is {0} bytes instead of {1}",
                                    expectedSize, lengthOfBinaryFile)
                        );
                }

                //Check was successful in case no error occured
                return errors.Count == 0;
            }

            protected virtual void setHeader(String header, Int64 lengthBinaryFile){
                List<String> errors;
                ENVI_FileReader.RequiredHeaderValues HV;
                if(!ENVI_FileReader.checkHeader(header, lengthBinaryFile, out errors, out HV)) {
                    throw new Exception(TextHelper.combine(errors, "\n"));
                } else {
                    this.HV = HV;
                }
            }

            protected Int32 getPosition(Int32 iS, Int32 iL, Int32 iB) {
                Int32 position = -1;
                if(HV.interleave == Interleave.BSQ){
                    int oB = HV.samples * HV.lines;
                    int oL = HV.samples;
                    position = iB * oB + iL * oL + iS;
                
                }else if(HV.interleave == Interleave.BIL){

                    throw new NotImplementedException(); 
                }else if(HV.interleave == Interleave.BIP){

                    throw new NotImplementedException(); 
                }else{
                    throw new Exception("unknown interleave");  
                }

                return position;
            }

            public T[, ,] readImage<T>(int s0, int s1, int l0, int l1, int b0, int b1) {
                int ns = s1-s0+1;
                int nl = l1-l0+1;
                int nb = b1-b0+1;
                T[,,] imageCube = new T[ns,nl,nb];
                
                if(HV.interleave == Interleave.BSQ) {
                    for(int iBand = 0; iBand < nb; iBand++) {
                        for(int iLine = 0; iLine < nl; iLine++) {
                            BR.setPosition(this.getPosition(s0, l0++, b0++));
                            T[] line = BR.ReadArray<T>(ns);
                            for(int iSample = 0; iSample < ns; iSample++) {
                                imageCube[iSample, iLine, iBand] = line[iSample];
                            }
                        }
                    }
                } else if(HV.interleave == Interleave.BIL) {
                    throw new NotImplementedException();
                } else if(HV.interleave == Interleave.BIP) {
                    throw new NotImplementedException();
                } else {
                    throw new Exception("unknown interleave");
                }

                return imageCube;
            }

        
            public static Type getCLIDataType(IDLDataType dt) {
                switch(dt) {
                    case IDLDataType.Byte_byte: return typeof(Byte);
                    case IDLDataType.Int16_int: return typeof(Int16);
                    case IDLDataType.Int32_long: return typeof(Int32);
                    case IDLDataType.Int64_long64: return typeof(Int64);
                    case IDLDataType.UInt16_uint: return typeof(UInt16);
                    case IDLDataType.UInt32_ulong: return typeof(UInt32);
                    case IDLDataType.UInt64_ulong64: return typeof(UInt64);
                    case IDLDataType.Complex32Bit_complex: throw new NotImplementedException();
                    case IDLDataType.Complex64Bit_dcomplex: throw new NotImplementedException();
                    case IDLDataType.Double_double: return typeof(Double);
                    case IDLDataType.Single_float: return typeof(Single);
                    default: throw new NotImplementedException();
                }
            }


            protected static Int32[] ParseInt32Array(String s) {
                return (from part in ENVI_FileReader.ParseStringArray(s)
                        select Int32.Parse(part, ENVI_FileReader.numericCulture)).ToArray();
            }

            protected static Double[] ParseDoubleArray(String s) {
                return (from part in ENVI_FileReader.ParseStringArray(s) 
                        select Double.Parse(part, ENVI_FileReader.numericCulture)).ToArray();            
            }

            protected static Single[] ParseSingleArray(String s) {
                return (from part in ENVI_FileReader.ParseStringArray(s)
                        select Single.Parse(part, ENVI_FileReader.numericCulture)).ToArray();
            }

            protected static Boolean[] ParseBooleanArray(String s, String isTrue="1", String isFalse="0") {
                return (from part in ENVI_FileReader.ParseStringArray(s)
                        select ENVI_FileReader.ParseBoolean(part)).ToArray();
            }

            protected static Boolean ParseBoolean(String s, String isFalseString="0") {
                s = s.Trim();
                return s != isFalseString;
            }
            
            protected static String[] ParseStringArray(String s) {
                return s.Split(new Char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            }


            public static ByteOrder ParseByteOrder(String s) {
                byte b = byte.Parse(s);
                switch(b) {
                    case 0: return ByteOrder.Host_Intel_LSF;
                    case 1: return ByteOrder.Network_IEEE_MSF;
                    default: throw new NotImplementedException();
                }
            
            }
            public static IDLDataType ParseIDLDataType(String s) {
                byte b = byte.Parse(s);
                switch(b) {
                    case 1: return IDLDataType.Byte_byte;
                    case 2: return IDLDataType.Int16_int;
                    case 3: return IDLDataType.Int32_long;
                    case 4: return IDLDataType.Single_float;
                    case 5: return IDLDataType.Double_double;
                    case 6: return IDLDataType.Complex32Bit_complex;
                    case 7: throw new NotImplementedException("IDL Data Type STRING");
                    case 8: throw new NotImplementedException("IDL Data Type STRUCT");
                    case 9: return IDLDataType.Complex64Bit_dcomplex;
                    case 10: throw new NotImplementedException("IDL Data Type POINTER");
                    case 11: throw new NotImplementedException("IDL Data Type OBJREF");
                    case 12: return IDLDataType.UInt16_uint;
                    case 13: return IDLDataType.UInt32_ulong;
                    case 14: return IDLDataType.Int64_long64;
                    case 15: return IDLDataType.UInt64_ulong64;
                    default: throw new NotImplementedException();

                }
            }
            

            /// <summary>
            /// Takes a string and tries to parse the interleave
            /// </summary>
            /// <param name="s"></param>
            /// <returns></returns>
            public static Interleave ParseInterleave(String s) {
                if(Regex.IsMatch(s, "BSQ", RegexOptions.IgnoreCase)) return Interleave.BSQ;
                if(Regex.IsMatch(s, "BIL", RegexOptions.IgnoreCase)) return Interleave.BIL;
                if(Regex.IsMatch(s, "BIP", RegexOptions.IgnoreCase)) return Interleave.BIP;
                throw new Exception("Can not parse interleave from:"+s);
            }

            /// <summary>
            /// Returns the size in bytes of an IDLDataType
            /// </summary>
            /// <param name="dt"></param>
            /// <returns></returns>
            public static byte getDataTypeByteSize(IDLDataType dt) {
                switch(dt) {
                    case IDLDataType.Byte_byte: return 1;
                    case IDLDataType.Int16_int: return 2;
                    case IDLDataType.Int32_long: return 4;
                    case IDLDataType.Int64_long64: return 8;
                    case IDLDataType.UInt16_uint: return 2;
                    case IDLDataType.UInt32_ulong: return 4;
                    case IDLDataType.UInt64_ulong64: return 8;
                    case IDLDataType.Complex32Bit_complex: return 8;
                    case IDLDataType.Complex64Bit_dcomplex: return 16;
                    case IDLDataType.Double_double: return 8;
                    case IDLDataType.Single_float: return 4;
                    default: throw new NotImplementedException();
                }
            }
            
            /// <summary>
            /// Return true if the input file name can be related to a
            /// binary file and it's ENVI header (*.hdr) file
            /// </summary>
            /// <param name="inputFile"></param>
            /// <param name="binaryFile"></param>
            /// <param name="headerFile"></param>
            /// <returns></returns>
            public static bool findFileInfos(FileInfo inputFile, out FileInfo binaryFile, out FileInfo headerFile) {
                binaryFile = null;
                headerFile = null;

                binaryFile = inputFile;
                //find header file
                String hdr1 = inputFile.FullName + ".hdr";
                String hdr2 = Regex.Replace(inputFile.FullName, @"\.[^\.]+$", ".hdr", RegexOptions.IgnoreCase);
                if(File.Exists(hdr1)) {
                    headerFile = new FileInfo(hdr1);
                } else if(File.Exists(hdr2)) {
                    headerFile = new FileInfo(hdr2);
                }
                return headerFile != null;
            }
            
    }
}
